# 6. Kubernetes之深入理解Pod对象

# Pod对象

  1. 最小部署单元
  2. 一组容器的集合
  3. 一个Pod中的容器共享网络命名空间
  4. Pod是短暂的

# Pod中如何管理多个容器

  1. Pod中可以同时运行多个进程(作为容器运行)协同工作。
  2. 同一个Pod中的容器会自动的分配到同一个 node 上。
  3. 同一个Pod中的容器共享资源、网络环境和依赖,它们总是被同时调度。

注意在一个Pod中同时运行多个容器是一种比较高级的用法

  1. 只有当你的容器需要紧密配合协作的时候才考虑用这种模式
  2. 例如,你有一个容器作为web服务器运行,需要用到共享的volume,有另一个“sidecar”容器来从远端获取资源更新这些文件,如下图所示

J4ddFf.png

# Pod中可以共享两种资源

  1. 网络:
    1. 每个Pod都会被分配一个唯一的IP地址。
    2. Pod中的所有容器共享网络空间,包括IP地址和端口。
    3. Pod内部的容器可以使用localhost(主机名)互相通信。
    4. Pod中的容器与外界通信时,必须分配共享网络资源(例如使用宿主机的端口映射)。
  2. 存储:
    1. 可以Pod指定多个共享的Volume。
    2. Pod中的所有容器都可以访问共享的volume。
    3. Volume也可以用来持久化Pod中的存储资源,以防容器重启后文件丢失

# Pod容器分类

  1. Infrastructure Container:基础容器,维护整个Pod网络空间
  2. InitContainers:初始化容器,先于业务容器开始执行
  3. Containers:业务容器,并行启动

# 镜像拉取策略(imagePullPolicy)

  1. IfNotPresent:默认值,镜像在宿主机上不存在时才拉取
  2. Always:每次创建Pod 都会重新拉取一次镜像
  3. Never:Pod 永远不会主动拉取这个镜像

# 镜像拉取策略 - 局部

  1. 写在containers中,只针对containers中的镜像拉取策略生成
  2. 就相当于编程中的局部配置跟全局配置一样
apiVersion: v1
kind: Pod
metadata:
  name: foo
  namespace: awesomeapps
spec:
  containers:
    - name: foo
      image: janedoe/awesomeapp:v1
      imagePullPolicy: IfNotPresent 

# 镜像拉取策略 - 全局

  1. 写在spec中,只针对spec所有的containers中的镜像拉取策略生成
  2. 就相当于编程中的局部配置跟全局配置一样
apiVersion: v1
kind: Pod
metadata:
  name: foo
  namespace: awesomeapps
spec:
  containers:
    - name: foo
      image: janedoe/awesomeapp:v1
  imagePullSecrets:
    - name: myregistrykey

# 资源限制

官方相关文档地址:https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/

  1. 创建Pod的时候,可以指定计算资源(目前支持的资源类型有CPU和内存),即指定每个容器的资源请求(Request)和资源限制(Limit),资源请求是容器所需的最小资源需求,资源限制则是容器不能超过的资源上限。它们的大小关系是:0<=request<=limit<=infinity
  2. Pod的资源请求就是Pod中容器资源请求之和。Kubernetes在调度Pod时,会根据Node中的资源总量(通过cAdvisor接口获得),以及该Node上已使用的计算资源,来判断该Node是否满足需求。
  3. 资源请求能够保证Pod有足够的资源来运行,而资源限制则是防止某个Pod无限制地使用资源,导致其他Pod崩溃。特别是在公有云场景,往往会有恶意软件通过抢占内存来攻击平台。
  4. 原理:Docker 通过使用Linux Cgroup来实现对容器资源的控制,具体到启动参数上是--memory和--cpu-shares。Kubernetes中是通过控制这两个参数来实现对容器资源的控制。

# Pod和Container的资源请求和限制

  1. spec.containers[].resources.limits.cpu
  2. spec.containers[].resources.limits.memory
  3. spec.containers[].resources.requests.cpu
  4. spec.containers[].resources.requests.memory

# 实例

以下Pod有两个容器

  1. 每个Container都有0.25 cpu和64MiB内存的请求。
  2. 每个Container的内存限制为0.5 cpu和128MiB。
  3. 你可以说Pod有0.5 cpu和128 MiB内存的请求,并且限制为1 cpu和256MiB的内存

以下是yaml文件

apiVersion: v1
kind: Pod
metadata:
  name: frontend
spec:
  containers:
  - name: db
    image: mysql
    env:
    - name: MYSQL_ROOT_PASSWORD
      value: "password"
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"
  - name: wp
    image: wordpress
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

# 查看node节点资源使用情况

kubectl describe pod frontend | grep -A 3 Events
Events:
  Type     Reason          Age                From                    Message
  ----     ------          ----               ----                    -------
  Normal   Scheduled       19m                default-scheduler       Successfully assigned default/frontend to 192.168.0.125
  


kubectl describe nodes 192.168.0.125
---------                  ----                                  ------------  ----------  ---------------  -------------  ---
  default                    frontend                              500m (25%)    1 (50%)     128Mi (3%)       256Mi (6%)     20m
  default                    tomcat-deployment-6bb6864d4f-drcjc    0 (0%)        0 (0%)      0 (0%)           0 (0%)         3h25m
  default                    tomcat-deployment-6bb6864d4f-wmkxx    0 (0%)        0 (0%)      0 (0%)           0 (0%)         3h25m
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests    Limits
  --------           --------    ------
  cpu                500m (25%)  1 (50%)
  memory             128Mi (3%)  256Mi (6%)
  ephemeral-storage  0 (0%)      0 (0%)
  
# 因输出的内容太多了,所以我只截了部分可看的内容

# 重启策略(restartPolicy)

  1. Always:当容器终止退出后,总是重启容器,默认策略。
  2. OnFailure:当容器异常退出(退出状态码非0)时,才重启容器。
  3. Never:当容器终止退出,从不重启容器
apiVersion: v1
kind: Pod
metadata:
  name: foo
  namespace: awesomeapps
spec:
  containers:
    - name: foo
    image: janedoe/awesomeapp:v1
    restartPolicy: Always

## 一般都是使用默认的Always

# 健康检查(Probe)

官方相关文档地址:https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/

  1. 在实际生产环境中,想要使得开发的应用程序完全没有bug,在任何时候都运行正常,几乎是不可能的任务。
  2. 因此,我们需要一套管理系统,来对用户的应用程序执行周期性的健康检查和修复操作。
  3. 这套管理系统必须运行在应用程序之外,这一点非常重要一一如果它是应用程序的一部分,极有可能会和应用程序一起崩溃。
  4. 因此,在Kubernetes中,系统和应用程序的健康检查是由Kubelet来完成的。

# 二种健康检查

  1. 进程级健康检查
    1. 最简单的健康检查是进程级的健康检查,即检验容器进程是否存活。
    2. 这类健康检查的监控粒度是在Kubernetes集群中运行的单一容器。
    3. Kubelet会定期通过Docker Daemon获取所有Docker进程的运行情况,如果发现某个Docker容器未正常运行,则重新启动该容器进程。
    4. 目前,进程级的健康检查都是默认启用的。
  2. 业务级健康检查
    1. 在很多实际场景下,仅仅使用进程级健康检查还远远不够。
    2. 有时,从Docker的角度来看,容器进程依旧在运行
    3. 但是如果从应用程序的角度来看,代码处于死锁状态,
    4. 即容器永远都无法正常响应用户的业务为了解决以上问题
    5. Kubernetes引人了一个在容器内执行的活性探针的概念,以支持用户自己实现应用业务级的健康检查

# Probe有以下两种类型

  1. livenessProbe:如果检查失败,将杀死容器,根据Pod的restartPolicy来操作。
  2. readinessProbe:如果检查失败,Kubernetes会把Pod从service endpoints中剔除

J5M50e.png

# Probe支持三种检查方法

  1. Exec(ExecAction):在容器内执行指定命令。如果命令退出时返回码为 0 则认为诊断成功。
  2. TCPSocket(TCPSocketAction):对指定端口上的容器的 IP 地址进行 TCP 检查。如果端口打开,则诊断被认为是成功的。
  3. HTTPGet(HTTPGetAction):对指定的端口和路径上的容器的 IP 地址执行 HTTP Get 请求。如果响应的状态码大于等于200 且小于 400,则诊断被认为是成功的。

# 一些简单的参数

  1. exec command 的方式探测 例如 ps 一个进程
  2. failureThreshold 探测几次失败 才算失败 默认是连续三次
  3. periodSeconds 每次的多长时间探测一次 默认10s
  4. timeoutSeconds 探测超市的秒数 默认1s
  5. initialDelaySeconds 初始化延迟探测,第一次探测的时候,因为主程序未必启动完成
  6. tcpSocket 检测端口的探测
  7. httpGet http请求探测

# exec探针检查方式的示例

# 编写yaml文件

在yaml配置文件中,您可以看到Pod具有单个Container

  1. 该periodSeconds字段指定kubelet应每5秒执行一次活跃度探测。
  2. 该initialDelaySeconds字段告诉kubelet它应该在执行第一次探测之前等待5秒。
  3. 要执行探测,kubelet将cat /tmp/healthy在Container中执行命令。
  4. 如果命令成功,则返回0,并且kubelet认为Container是活动且健康的。
  5. 如果该命令返回非零值,则kubelet会终止Container并重新启动它
apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: liveness-exec
spec:
  containers:
  - name: liveness
    image: busybox
    args:
    - /bin/sh
    - -c
    - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
    livenessProbe:
      exec:
        command:
        - cat
        - /tmp/healthy
      initialDelaySeconds: 5
      periodSeconds: 5

# 使用yaml文件生成示例

kubectl create -f exec-liveness.yaml 
pod/liveness-exec created


kubectl get pod
NAME                                 READY   STATUS    RESTARTS   AGE
frontend                             2/2     Running   106        16h
liveness-exec                        1/1     Running   0          16s

再等30秒,确认Container已重新启动,RESTARTS已增加

# kubernetes中pod创建流程

  1. Pod是Kubernetes中最基本的部署调度单元,可以包含container,逻辑上表示某种应用的一个实例。
  2. 例如一个web站点应用由前端、后端及数据库构建而成,这三个组件将运行在各自的容器中,那么我们可以创建包含三个container的pod
  3. 以下是创建流程图

J4yRKS.png

J5JXPe.png

  1. 客户端提交创建请求:
    1. 可以通过API Server的Restful API,也可以使用kubectl命令行工具。
    2. 支持的数据类型包括JSON和YAML。
  2. API Server:处理用户请求,存储Pod数据到etcd。
  3. 调度器:通过API Server查看未绑定的Pod,尝试为Pod分配主机。
  4. 过滤主机 (调度预选):
    1. 调度器用一组规则过滤掉不符合要求的主机。
    2. 比如Pod指定了所需要的资源量,那么可用资源比Pod需要的资源量少的主机会被过滤掉。
  5. 主机打分(调度优选):
    1. 对第一步筛选出的符合要求的主机进行打分,在主机打分阶段,调度器会考虑一些整体优化策略
    2. 比如把容一个Replication Controller的副本分布到不同的主机上,使用最低负载的主机等。
  6. 选择主机:选择打分最高的主机,进行binding操作,结果存储到etcd中。
  7. kubelet根据调度结果执行Pod创建操作:
    1. 绑定成功后,scheduler会调用APIServer的API在etcd中创建一个boundpod对象,描述在一个工作节点上绑定运行的所有pod信息。
    2. 运行在每个工作节点上的kubelet也会定期与etcd同步boundpod信息,一旦发现应该在该工作节点上运行的boundpod对象没有更新,则调用Docker API创建并启动pod内的容器。

# 调度约束

官方相关文档地址:https://kubernetes.io/docs/concepts/configuration/assign-pod-node/

可以约束Pod (opens new window)只能在特定节点 (opens new window)上运行 ,或者更喜欢在特定节点上运行

  1. 有几种方法可以做到这一点,推荐的方法都使用 标签选择器 (opens new window)进行选择。
  2. 通常,此类约束是不必要的,因为调度程序会自动进行合理的放置
    1. 例如,将Pod分散到节点上,而不是将Pod放置在可用资源不足的节点上,等等
  3. 但是在某些情况下,您可能需要更多控制一个Pod降落的节点,
    1. 例如,以确保Pod最终落在连接了SSD的机器上,或将来自两个不同服务的Pod共同放置在一个可用区域中,这些服务之间的通信量很大

# 调度器的工作机制

  1. 预备工作
    1. 缓存所有的node节点,记录他们的规格:cpu、内存、磁盘空间、gpu显卡数等;
    2. 缓存所有运行中的pod,按照pod所在的node进行区分,统计每个node上的pod request了多少资源。request是pod的QoS配置。
    3. list & watch pod资源,当检查到有新的Pending状态的pod出现,就将它加入到调度队列中。
    4. 调度器的worker组件从队列中取出pod进行调度。
  2. 调度过程
    1. 先将当前所有的node放入队列;
    2. 执行predicates算法,对队列中的node进行筛选。这里算法检查了一些pod运行的必要条件,包括port不冲突、cpu和内存资源QoS(如果有的话)必须满足、挂载volume(如果有的话)类型必须匹配、nodeSelector规则必须匹配、硬性的affinity规则(下文会提到)必须匹配、node的状态(condition)必须正常,taint_toleration硬规则(下文会提到)等等。
    3. 执行priorities算法,对队列中剩余的node进行评分,这里有许多评分项,各个项目有各自的权重:整体cpu,内存资源的平衡性、node上是否有存在要求的镜像、同rs的pod是否有调度、node affinity的软规则、taint_toleration软规则(下文会提到)等等。
    4. 最终评分最高的node会被选出。即代码中suggestedHost, err := sched.schedule(pod)一句(plugin/pkg/scheduler/scheduler.go)的返回值。
    5. 调度器执行assume方法,该方法在pod调度到node之前,就以“该pod运行在目标node上” 为场景更新调度器缓存中的node 信息,也即预备工作中的1、2两点。这么做是为了让pod在真正调度到node上时,调度器也可以同时做后续其他pod的调度工作。
    6. 调度器执行bind方法,该方法创建一个Binding资源,apiserver检查到创建该资源时,会主动更新pod的nodeName字段。完成调度

# 调度约束的二种机制

  1. nodeName用于将Pod调度到指定的Node名称上
  2. nodeSelector用于将Pod调度到匹配Label的Node上

# nodeName - 强制指定

nodeName用于将Pod调度到指定的Node名称

spec.nodeName用于强制约束将Pod调度到指定的Node节点上,这里说是“调度”,但其实指定了nodeName的Pod会直接跳过Scheduler的调度逻辑,直接写入PodList列表,该匹配规则是强制匹配

apiVersion: v1
kind: Pod
metadata:
  name: pod-example
  labels:
    app: nginx
spec:
  nodeName: 192.168.0.125
  containers:
  - name: nginx
    image: nginx:1.15
kubectl create -f pod3.yaml 
pod/pod-example created


kubectl get pod
NAME          READY   STATUS    RESTARTS   AGE
nginx         1/1     Running   0          10m
pod-example   1/1     Running   0          22s


kubectl describe pod pod-example
  Type    Reason   Age   From                    Message
  ----    ------   ----  ----                    -------
  Normal  Pulled   31s   kubelet, 192.168.0.125  Container image "nginx:1.15" already present on machine
  Normal  Created  31s   kubelet, 192.168.0.125  Created container
  Normal  Started  30s   kubelet, 192.168.0.125  Started container
  
## 因返回结果过多,所以只截很小一部分

# nodeSelector - 匹配

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
  nodeSelector:
    env_role: dev
    
## env_role: dev, 是匹配带有这标签的node机器, 如果带有这标签的node节点有2台, 那么就会在这二台中选下部署
kubectl create -f pod2.yaml 
pod/nginx created


kubectl get pod
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          115s


kubectl describe pod nginx
 Type    Reason     Age    From                    Message
  ----    ------     ----   ----                    -------
  Normal  Scheduled  2m11s  default-scheduler       Successfully assigned default/nginx to 192.168.0.126
  Normal  Pulled     2m11s  kubelet, 192.168.0.126  Container image "nginx" already present on machine
  Normal  Created    2m10s  kubelet, 192.168.0.126  Created container
  Normal  Started    2m10s  kubelet, 192.168.0.126  Started container
## 因返回结果过多,所以只截很小一部分

# 故障排查

J5YgsI.png

kubectl describe TYPE/NAME
kubectl logs TYPE/NAME [-c CONTAINER]
kubectl exec POD [-c CONTAINER] --COMMAND [args...]